package com.kaibes.core.spring.bean;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import com.kaibes.core.base.util.ListUtils;

public class BeanUtils extends org.springframework.beans.BeanUtils {

    public static void filterOject(Object object, BeanFilter filter) {
        if (object == null || filter == null) {
            return;
        }

        Field[] objectFields = object.getClass().getDeclaredFields();
        List<String> fileds = filter.getFields();
        if (filter.isIncluded()) {
            for (Field field : objectFields) {
                field.setAccessible(true);
                if (!fileds.contains(field.getName())) {
                    try {
                        field.set(object, null);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        } else {
            for (Field field : objectFields) {
                field.setAccessible(true);
                if (fileds.contains(field.getName())) {
                    try {
                        field.set(object, null);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static List<String> getFieldNameList(Class<?> clazz, BeanFilter filter) {
        if (filter == null) {
            return null;
        }

        Field[] fields = clazz.getDeclaredFields();
        List<String> fieldNames = new ArrayList<String>();
        if (filter.isIncluded()) {
            if (filter.getFields() == null) {
                return null;
            } else {
                for (Field field : fields) {
                    if (filter.getFields().contains(field.getName())) {
                        fieldNames.add(field.getName());
                    }
                }
            }
        } else {
            if (filter.getFields() == null) {
                for (Field field : fields) {
                    fieldNames.add(field.getName());
                }
            } else {
                for (Field field : fields) {
                    if (!filter.getFields().contains(field.getName())) {
                        fieldNames.add(field.getName());
                    }
                }
            }
        }
        return fieldNames;
    }

    public static <T> T createTarget(Object source, Class<T> targetClass) {
        return createTarget(source, targetClass, null);
    }

    @SuppressWarnings("unchecked")
    public static <T> T createTarget(Object source, Class<T> targetClass, BeanFilter filter) {
        if (source.getClass() == targetClass && filter == null) {
            return (T) source;
        }

        try {
            T target = targetClass.getDeclaredConstructor().newInstance();
            if (filter == null) {
                copyProperties(source, target);
            } else {
                if (filter.isIncluded()) {
                    copyPropertiesIncluded(source, target, filter.getFields());
                } else {
                    copyPropertiesExcluded(source, target, filter.getFields());
                }
            }
            return target;
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("rawtypes")
    private static void copyPropertiesExcluded(Object source, Object target, List<String> propertyList)
            throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);

        if (source instanceof Map) {
            Map map = (Map) source;
            for (PropertyDescriptor targetPd : targetPds) {
                Method writeMethod = targetPd.getWriteMethod();
                if (writeMethod != null && ListUtils.nullOrNotContains(propertyList, targetPd.getName())) {
                    Object value = map.get(targetPd.getName());
                    if (ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], value.getClass())) {
                        try {
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value);
                        } catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        } else {
            for (PropertyDescriptor targetPd : targetPds) {
                Method writeMethod = targetPd.getWriteMethod();
                if (writeMethod != null && ListUtils.nullOrNotContains(propertyList, targetPd.getName())) {
                    PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                    if (sourcePd != null) {
                        Method readMethod = sourcePd.getReadMethod();
                        if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0],
                                readMethod.getReturnType())) {
                            try {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }
                                Object value = readMethod.invoke(source);
                                if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                    writeMethod.setAccessible(true);
                                }
                                writeMethod.invoke(target, value);
                            } catch (Throwable ex) {
                                throw new FatalBeanException(
                                        "Could not copy property '" + targetPd.getName() + "' from source to target",
                                        ex);
                            }
                        }
                    }
                }
            }
        }
    }

    @SuppressWarnings("rawtypes")
    private static void copyPropertiesIncluded(Object source, Object target, List<String> propertyList)
            throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);

        if (source instanceof Map) {
            Map map = (Map) source;
            for (PropertyDescriptor targetPd : targetPds) {
                Method writeMethod = targetPd.getWriteMethod();
                if (writeMethod != null && propertyList != null && propertyList.contains(targetPd.getName())) {
                    Object value = map.get(targetPd.getName());
                    Class<?> sourceClazz = value.getClass();
                    Class<?> targetClazz = writeMethod.getParameterTypes()[0];

                    if (sourceClazz == BigInteger.class && targetClazz == Long.class) {
                        value = ((BigInteger) value).longValue();
                        try {
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value);
                        } catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    } else if (ClassUtils.isAssignable(sourceClazz, targetClazz)) {
                        try {
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value);
                        } catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        } else {
            for (PropertyDescriptor targetPd : targetPds) {
                Method writeMethod = targetPd.getWriteMethod();
                if (writeMethod != null && propertyList != null && propertyList.contains(targetPd.getName())) {
                    PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                    if (sourcePd != null) {
                        Method readMethod = sourcePd.getReadMethod();
                        if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0],
                                readMethod.getReturnType())) {
                            try {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }
                                Object value = readMethod.invoke(source);
                                if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                    writeMethod.setAccessible(true);
                                }
                                writeMethod.invoke(target, value);
                            } catch (Throwable ex) {
                                throw new FatalBeanException(
                                        "Could not copy property '" + targetPd.getName() + "' from source to target",
                                        ex);
                            }
                        }
                    }
                }
            }
        }

    }
}
